1 安装XGBoost

2 准备

2.1 载入XGBoost

require(xgboost)
搼㸴搼㸸挼㸸攼㹢搼㸰攼㸸Ҫ戼㸵ij̼愼㹤戼㸰昼㹣愼㸳戼㹡xgboost

2.2 载入Mushroom数据集

data(agaricus.train, package='xgboost')
data(agaricus.test, package='xgboost')
train <- agaricus.train
test <- agaricus.test
  • 数据集中包含了datalabel两个元素
str(train)
List of 2
 $ data :Formal class 'dgCMatrix' [package "Matrix"] with 6 slots
  .. ..@ i       : int [1:143286] 2 6 8 11 18 20 21 24 28 32 ...
  .. ..@ p       : int [1:127] 0 369 372 3306 5845 6489 6513 8380 8384 10991 ...
  .. ..@ Dim     : int [1:2] 6513 126
  .. ..@ Dimnames:List of 2
  .. .. ..$ : NULL
  .. .. ..$ : chr [1:126] "cap-shape=bell" "cap-shape=conical" "cap-shape=convex" "cap-shape=flat" ...
  .. ..@ x       : num [1:143286] 1 1 1 1 1 1 1 1 1 1 ...
  .. ..@ factors : list()
 $ label: num [1:6513] 1 0 0 1 0 0 0 1 0 0 ...
  • 数据集的维度
dim(train$data)
[1] 6513  126
dim(test$data)
[1] 1611  126
  • data以稀疏矩阵的形式储存在dgCMatrix中,label{0,1}数值向量
class(train$data)[1]
[1] "dgCMatrix"
class(train$label)
[1] "numeric"

3 训练初步的XGBoost模型

bstSparse <- xgboost(data = train$data, label = train$label, max_depth = 2, eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic")
[1] train-error:0.046522 
[2] train-error:0.022263 
bstDense <- xgboost(data = as.matrix(train$data), label = train$label, max_depth = 2, eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic")
[1] train-error:0.046522 
[2] train-error:0.022263 
dtrain <- xgb.DMatrix(data = train$data, label = train$label)
bstDMatrix <- xgboost(data = dtrain, max_depth = 2, eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic")
[1] train-error:0.046522 
[2] train-error:0.022263 
# verbose = 0, no message
bst <- xgboost(data = dtrain, max_depth = 2, eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic", verbose = 0)
# verbose = 1, print evaluation metric
bst <- xgboost(data = dtrain, max_depth = 2, eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic", verbose = 1)
[1] train-error:0.046522 
[2] train-error:0.022263 
# verbose = 2, also print information about tree
bst <- xgboost(data = dtrain, max_depth = 2, eta = 1, nthread = 2, nrounds = 2, objective = "binary:logistic", verbose = 2)
[18:21:12] amalgamation/../src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 6 extra nodes, 0 pruned nodes, max_depth=2
[1] train-error:0.046522 
[18:21:12] amalgamation/../src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 4 extra nodes, 0 pruned nodes, max_depth=2
[2] train-error:0.022263 

4 使用XGBoost进行初步预测

pred <- predict(bst, test$data)
# size of the prediction vector
print(length(pred))
[1] 1611
# limit display of predictions to the first 10
print(head(pred))
[1] 0.28583017 0.92392391 0.28583017 0.28583017 0.05169873 0.92392391
prediction <- as.numeric(pred > 0.5)
print(head(prediction))
[1] 0 1 0 0 0 1

5 衡量模型表现

err <- mean(as.numeric(pred > 0.5) != test$label)
print(paste("test-error=", err))
[1] "test-error= 0.0217256362507759"

6 高级特性

dtrain <- xgb.DMatrix(data = train$data, label=train$label)
dtest <- xgb.DMatrix(data = test$data, label=test$label)

6.1 监控训练过程,防止过拟合

  • xgb.train来衡量模型训练的过程,利用watchlist参数,防止过多的训练轮次导致过拟合:
watchlist <- list(train=dtrain, test=dtest)
bst <- xgb.train(data=dtrain, max_depth=2, eta=1, nthread = 2, nrounds=2, watchlist=watchlist, objective = "binary:logistic")
[1] train-error:0.046522    test-error:0.042831 
[2] train-error:0.022263    test-error:0.021726 
  • 利用xgb.csv配合early_stopping_rounds参数防止过拟合:
params = list(
  objective = "binary:logistic",
  eta=1,
  max_depth=2,
  nthread=2
)
bst <- xgb.cv(params=params, data=dtrain, nrounds=200, nfold=5, early_stopping_rounds=5, metrics = list("auc"))
[1] train-auc:0.958221+0.001361 test-auc:0.958115+0.005430 
Multiple eval metrics are present. Will use test_auc for early stopping.
Will train until test_auc hasn't improved in 5 rounds.

[2] train-auc:0.981411+0.000870 test-auc:0.981378+0.003471 
[3] train-auc:0.997073+0.000346 test-auc:0.997112+0.001378 
[4] train-auc:0.998757+0.000109 test-auc:0.998757+0.000454 
[5] train-auc:0.999299+0.000104 test-auc:0.999310+0.000414 
[6] train-auc:0.999504+0.000070 test-auc:0.999466+0.000421 
[7] train-auc:0.999673+0.000067 test-auc:0.999631+0.000295 
[8] train-auc:0.999757+0.000183 test-auc:0.999621+0.000380 
[9] train-auc:0.999912+0.000076 test-auc:0.999902+0.000079 
[10]    train-auc:0.999979+0.000023 test-auc:0.999965+0.000036 
[11]    train-auc:1.000000+0.000000 test-auc:1.000000+0.000000 
[12]    train-auc:1.000000+0.000000 test-auc:1.000000+0.000000 
[13]    train-auc:1.000000+0.000000 test-auc:1.000000+0.000000 
[14]    train-auc:1.000000+0.000000 test-auc:1.000000+0.000000 
[15]    train-auc:1.000000+0.000000 test-auc:1.000000+0.000000 
[16]    train-auc:1.000000+0.000000 test-auc:1.000000+0.000000 
Stopping. Best iteration:
[11]    train-auc:1.000000+0.000000 test-auc:1.000000+0.000000
  • 可以通过eval_metric设置多种衡量模型效果的方式:
bst <- xgb.train(data=dtrain, max_depth=2, eta=1, nthread = 2, nrounds=2, watchlist=watchlist, eval_metric = "error", eval_metric = "logloss", objective = "binary:logistic")
[1] train-error:0.046522    train-logloss:0.233376  test-error:0.042831 test-logloss:0.226686 
[2] train-error:0.022263    train-logloss:0.136658  test-error:0.021726 test-logloss:0.137874 

6.2 线性提升器

之前的XGBoost都是基于提升树,除此之外,还可以建立基于线性提升器的模型。设置参数booster = "gblinear",同时移除eta参数:

bst <- xgb.train(data=dtrain, booster = "gblinear", max_depth=2, nthread = 2, nrounds=2, watchlist=watchlist, eval_metric = "error", eval_metric = "logloss", objective = "binary:logistic")
[1] train-error:0.028405    train-logloss:0.189791  test-error:0.023588 test-logloss:0.186855 
[2] train-error:0.004453    train-logloss:0.072454  test-error:0.004345 test-logloss:0.071301 

对于小的数据集,由于真实情况可能是线性可分的,线性提升器的效果可能更好。所以在建模时,建议尝试两种提升器并比较效果。

7 操纵xgb.DMatrix

xgb.DMatrix.save(dtrain, "dtrain.buffer")
[1] TRUE
# to load it in, simply call xgb.DMatrix
dtrain2 <- xgb.DMatrix("dtrain.buffer")
[18:21:13] 6513x126 matrix with 143286 entries loaded from dtrain.buffer
bst <- xgb.train(data=dtrain2, max_depth=2, eta=1, nthread = 2, nrounds=2, watchlist=watchlist, objective = "binary:logistic")
[1] train-error:0.046522    test-error:0.042831 
[2] train-error:0.022263    test-error:0.021726 
file.remove("dtrain.buffer")
[1] TRUE
label = getinfo(dtest, "label")
pred <- predict(bst, dtest)
err <- as.numeric(sum(as.integer(pred > 0.5) != label))/length(label)
print(paste("test-error=", err))
[1] "test-error= 0.0217256362507759"

8 观察特征重要性和模型

importance_matrix <- xgb.importance(model = bst)
print(importance_matrix)
xgb.plot.importance(importance_matrix = importance_matrix)

xgb.dump(bst, with_stats = T)
 [1] "booster[0]"                                                           
 [2] "0:[f28<-9.53674e-007] yes=1,no=2,missing=1,gain=4000.53,cover=1628.25"
 [3] "1:[f55<-9.53674e-007] yes=3,no=4,missing=3,gain=1158.21,cover=924.5"  
 [4] "3:leaf=1.71218,cover=812"                                             
 [5] "4:leaf=-1.70044,cover=112.5"                                          
 [6] "2:[f108<-9.53674e-007] yes=5,no=6,missing=5,gain=198.174,cover=703.75"
 [7] "5:leaf=-1.94071,cover=690.5"                                          
 [8] "6:leaf=1.85965,cover=13.25"                                           
 [9] "booster[1]"                                                           
[10] "0:[f59<-9.53674e-007] yes=1,no=2,missing=1,gain=832.545,cover=788.852"
[11] "1:[f28<-9.53674e-007] yes=3,no=4,missing=3,gain=569.725,cover=768.39" 
[12] "3:leaf=0.784718,cover=458.937"                                        
[13] "4:leaf=-0.96853,cover=309.453"                                        
[14] "2:leaf=-6.23624,cover=20.4624"                                        
library(DiagrammeR)
xgb.plot.tree(model = bst)
# save model to binary local file
xgb.save(bst, "xgboost.model")
[1] TRUE
# load binary model to R
bst2 <- xgb.load("xgboost.model")
pred2 <- predict(bst2, test$data)

比较原始模型和载入的模型是否完全一致

print(paste("sum(abs(pred2-pred))=", sum(abs(pred2-pred))))
[1] "sum(abs(pred2-pred))= 0"
file.remove("./xgboost.model")
[1] TRUE
LS0tDQp0aXRsZTogIlhHQm9vc3QgaW4gUiINCm91dHB1dDogaHRtbF9ub3RlYm9vaw0KLS0tDQoNCg0KIyAxIOWuieijhVhHQm9vc3QNCisgR2l0aHViIHZlcnNpb24NCmBgYA0KaW5zdGFsbC5wYWNrYWdlcygiZHJhdCIsIHJlcG9zPSJodHRwczovL2NyYW4ucnN0dWRpby5jb20iKQ0KZHJhdDo6OmFkZFJlcG8oImRtbGMiKQ0KaW5zdGFsbC5wYWNrYWdlcygieGdib29zdCIsIHJlcG9zPSJodHRwOi8vZG1sYy5tbC9kcmF0LyIsIHR5cGUgPSAic291cmNlIikNCmBgYA0KDQorIENSQU4gdmVyc2lvbg0KYGBgDQppbnN0YWxsLnBhY2thZ2VzKCJ4Z2Jvb3N0IikNCmBgYA0KDQojIDIg5YeG5aSHDQojIyAyLjEg6L295YWlWEdCb29zdA0KYGBge3J9DQpyZXF1aXJlKHhnYm9vc3QpDQpgYGANCg0KIyMgMi4yIOi9veWFpU11c2hyb29t5pWw5o2u6ZuGDQpgYGB7cn0NCmRhdGEoYWdhcmljdXMudHJhaW4sIHBhY2thZ2U9J3hnYm9vc3QnKQ0KZGF0YShhZ2FyaWN1cy50ZXN0LCBwYWNrYWdlPSd4Z2Jvb3N0JykNCnRyYWluIDwtIGFnYXJpY3VzLnRyYWluDQp0ZXN0IDwtIGFnYXJpY3VzLnRlc3QNCmBgYA0KDQorIOaVsOaNrumbhuS4reWMheWQq+S6hmBkYXRhYOWSjGBsYWJlbGDkuKTkuKrlhYPntKANCmBgYHtyfQ0Kc3RyKHRyYWluKQ0KYGBgDQoNCisg5pWw5o2u6ZuG55qE57u05bqmDQpgYGB7cn0NCmRpbSh0cmFpbiRkYXRhKQ0KZGltKHRlc3QkZGF0YSkNCmBgYA0KDQorIGBkYXRhYOS7peeogOeWj+efqemYteeahOW9ouW8j+WCqOWtmOWcqGBkZ0NNYXRyaXhg5Lit77yMYGxhYmVsYOaYr2B7MCwxfWDmlbDlgLzlkJHph48NCmBgYHtyfQ0KY2xhc3ModHJhaW4kZGF0YSlbMV0NCmNsYXNzKHRyYWluJGxhYmVsKQ0KYGBgDQoNCiMgMyDorq3nu4PliJ3mraXnmoRYR0Jvb3N05qih5Z6LDQorIOS9v+eUqOWmguS4i+WPguaVsOiuree7g+aooeWei++8mg0KICAgICsgYG9iamVjdGl2ZSA9ICJiaW5hcnk6bG9naXN0aWMiYO+8muiuree7g+S6jOWFg+WIhuexu+aooeWeiyAgDQogICAgKyBgbWF4X2RlcHRoID0gMmDvvJrnlLHkuo7mlbDmja7pm4blvojlsI/vvIzmoJHnmoTmnIDlpKfmt7Hluqborr7kuLoyICANCiAgICArIGBudGhyZWFkID0gMmDvvJrkvb/nlKgy5LiqY3B157q/56iLICANCiAgICArIGBucm91bmRzID0gMmDvvJrorq3nu4My6L2u5qih5Z6LICANCg0KYGBge3J9DQpic3RTcGFyc2UgPC0geGdib29zdChkYXRhID0gdHJhaW4kZGF0YSwgbGFiZWwgPSB0cmFpbiRsYWJlbCwgbWF4X2RlcHRoID0gMiwgZXRhID0gMSwgbnRocmVhZCA9IDIsIG5yb3VuZHMgPSAyLCBvYmplY3RpdmUgPSAiYmluYXJ5OmxvZ2lzdGljIikNCmBgYA0KDQorIOWwhuaVsOaNruS7peaZrumAmu+8iOeooOWvhu+8ieefqemYteeahOW9ouW8j+i+k+WFpe+8mg0KYGBge3J9DQpic3REZW5zZSA8LSB4Z2Jvb3N0KGRhdGEgPSBhcy5tYXRyaXgodHJhaW4kZGF0YSksIGxhYmVsID0gdHJhaW4kbGFiZWwsIG1heF9kZXB0aCA9IDIsIGV0YSA9IDEsIG50aHJlYWQgPSAyLCBucm91bmRzID0gMiwgb2JqZWN0aXZlID0gImJpbmFyeTpsb2dpc3RpYyIpDQpgYGANCg0KKyDlsIbmlbDmja7ku6VYR0Jvb3N05o+Q5L6b55qEYHhnYi5ETWF0cml4YOW9ouW8j+i+k+WFpe+8mg0KYGBge3J9DQpkdHJhaW4gPC0geGdiLkRNYXRyaXgoZGF0YSA9IHRyYWluJGRhdGEsIGxhYmVsID0gdHJhaW4kbGFiZWwpDQpic3RETWF0cml4IDwtIHhnYm9vc3QoZGF0YSA9IGR0cmFpbiwgbWF4X2RlcHRoID0gMiwgZXRhID0gMSwgbnRocmVhZCA9IDIsIG5yb3VuZHMgPSAyLCBvYmplY3RpdmUgPSAiYmluYXJ5OmxvZ2lzdGljIikNCmBgYA0KDQorIFZlcmJvc2XpgInpobnvvJoNCmBgYHtyfQ0KIyB2ZXJib3NlID0gMCwgbm8gbWVzc2FnZQ0KYnN0IDwtIHhnYm9vc3QoZGF0YSA9IGR0cmFpbiwgbWF4X2RlcHRoID0gMiwgZXRhID0gMSwgbnRocmVhZCA9IDIsIG5yb3VuZHMgPSAyLCBvYmplY3RpdmUgPSAiYmluYXJ5OmxvZ2lzdGljIiwgdmVyYm9zZSA9IDApDQpgYGANCg0KYGBge3J9DQojIHZlcmJvc2UgPSAxLCBwcmludCBldmFsdWF0aW9uIG1ldHJpYw0KYnN0IDwtIHhnYm9vc3QoZGF0YSA9IGR0cmFpbiwgbWF4X2RlcHRoID0gMiwgZXRhID0gMSwgbnRocmVhZCA9IDIsIG5yb3VuZHMgPSAyLCBvYmplY3RpdmUgPSAiYmluYXJ5OmxvZ2lzdGljIiwgdmVyYm9zZSA9IDEpDQpgYGANCg0KYGBge3J9DQojIHZlcmJvc2UgPSAyLCBhbHNvIHByaW50IGluZm9ybWF0aW9uIGFib3V0IHRyZWUNCmJzdCA8LSB4Z2Jvb3N0KGRhdGEgPSBkdHJhaW4sIG1heF9kZXB0aCA9IDIsIGV0YSA9IDEsIG50aHJlYWQgPSAyLCBucm91bmRzID0gMiwgb2JqZWN0aXZlID0gImJpbmFyeTpsb2dpc3RpYyIsIHZlcmJvc2UgPSAyKQ0KYGBgDQoNCiMgNCDkvb/nlKhYR0Jvb3N06L+b6KGM5Yid5q2l6aKE5rWLDQorIOWvuWB0ZXN0YOaVsOaNrumbhui/m+ihjOmihOa1i++8mg0KYGBge3J9DQpwcmVkIDwtIHByZWRpY3QoYnN0LCB0ZXN0JGRhdGEpDQoNCiMgc2l6ZSBvZiB0aGUgcHJlZGljdGlvbiB2ZWN0b3INCnByaW50KGxlbmd0aChwcmVkKSkNCg0KIyBsaW1pdCBkaXNwbGF5IG9mIHByZWRpY3Rpb25zIHRvIHRoZSBmaXJzdCAxMA0KcHJpbnQoaGVhZChwcmVkKSkNCmBgYA0KDQorIGBwcmVkaWN0YOi+k+WHuueahOaYr+amgueOh++8jOWwhuWFtui9rOaNouS4umB7MCwxfWDvvJoNCmBgYHtyfQ0KcHJlZGljdGlvbiA8LSBhcy5udW1lcmljKHByZWQgPiAwLjUpDQpwcmludChoZWFkKHByZWRpY3Rpb24pKQ0KYGBgDQoNCiMgNSDooaHph4/mqKHlnovooajnjrANCisg6K6h566X5rWL6K+V6ZuG5LiK55qE5bmz5Z2H6ZSZ6K+v546H77yaDQpgYGB7cn0NCmVyciA8LSBtZWFuKGFzLm51bWVyaWMocHJlZCA+IDAuNSkgIT0gdGVzdCRsYWJlbCkNCnByaW50KHBhc3RlKCJ0ZXN0LWVycm9yPSIsIGVycikpDQpgYGANCg0KIyA2IOmrmOe6p+eJueaApw0KKyDlh4blpIfmlbDmja7pm4bvvJoNCmBgYHtyfQ0KZHRyYWluIDwtIHhnYi5ETWF0cml4KGRhdGEgPSB0cmFpbiRkYXRhLCBsYWJlbD10cmFpbiRsYWJlbCkNCmR0ZXN0IDwtIHhnYi5ETWF0cml4KGRhdGEgPSB0ZXN0JGRhdGEsIGxhYmVsPXRlc3QkbGFiZWwpDQpgYGANCg0KIyMgNi4xIOebkeaOp+iuree7g+i/h+eoi++8jOmYsuatoui/h+aLn+WQiA0KKyDnlKhgeGdiLnRyYWluYOadpeihoemHj+aooeWei+iuree7g+eahOi/h+eoi++8jOWIqeeUqGB3YXRjaGxpc3Rg5Y+C5pWw77yM6Ziy5q2i6L+H5aSa55qE6K6t57uD6L2u5qyh5a+86Ie06L+H5ouf5ZCI77yaDQpgYGB7cn0NCndhdGNobGlzdCA8LSBsaXN0KHRyYWluPWR0cmFpbiwgdGVzdD1kdGVzdCkNCg0KYnN0IDwtIHhnYi50cmFpbihkYXRhPWR0cmFpbiwgbWF4X2RlcHRoPTIsIGV0YT0xLCBudGhyZWFkID0gMiwgbnJvdW5kcz0yLCB3YXRjaGxpc3Q9d2F0Y2hsaXN0LCBvYmplY3RpdmUgPSAiYmluYXJ5OmxvZ2lzdGljIikNCmBgYA0KDQorIOWIqeeUqGB4Z2IuY3N2YOmFjeWQiGBlYXJseV9zdG9wcGluZ19yb3VuZHNg5Y+C5pWw6Ziy5q2i6L+H5ouf5ZCI77yaDQpgYGB7cn0NCnBhcmFtcyA9IGxpc3QoDQogIG9iamVjdGl2ZSA9ICJiaW5hcnk6bG9naXN0aWMiLA0KICBldGE9MSwNCiAgbWF4X2RlcHRoPTIsDQogIG50aHJlYWQ9Mg0KKQ0KYnN0IDwtIHhnYi5jdihwYXJhbXM9cGFyYW1zLCBkYXRhPWR0cmFpbiwgbnJvdW5kcz0yMDAsIG5mb2xkPTUsIGVhcmx5X3N0b3BwaW5nX3JvdW5kcz01LCBtZXRyaWNzID0gbGlzdCgiYXVjIikpDQpgYGANCg0KKyDlj6/ku6XpgJrov4dgZXZhbF9tZXRyaWNg6K6+572u5aSa56eN6KGh6YeP5qih5Z6L5pWI5p6c55qE5pa55byP77yaDQpgYGB7cn0NCmJzdCA8LSB4Z2IudHJhaW4oZGF0YT1kdHJhaW4sIG1heF9kZXB0aD0yLCBldGE9MSwgbnRocmVhZCA9IDIsIG5yb3VuZHM9Miwgd2F0Y2hsaXN0PXdhdGNobGlzdCwgZXZhbF9tZXRyaWMgPSAiZXJyb3IiLCBldmFsX21ldHJpYyA9ICJsb2dsb3NzIiwgb2JqZWN0aXZlID0gImJpbmFyeTpsb2dpc3RpYyIpDQpgYGANCg0KIyMgNi4yIOe6v+aAp+aPkOWNh+WZqA0K5LmL5YmN55qEWEdCb29zdOmDveaYr+WfuuS6juaPkOWNh+agke+8jOmZpOatpOS5i+Wklu+8jOi/mOWPr+S7peW7uueri+WfuuS6jue6v+aAp+aPkOWNh+WZqOeahOaooeWei+OAguiuvue9ruWPguaVsGBib29zdGVyID0gImdibGluZWFyImDvvIzlkIzml7bnp7vpmaRgZXRhYOWPguaVsO+8mg0KYGBge3J9DQpic3QgPC0geGdiLnRyYWluKGRhdGE9ZHRyYWluLCBib29zdGVyID0gImdibGluZWFyIiwgbWF4X2RlcHRoPTIsIG50aHJlYWQgPSAyLCBucm91bmRzPTIsIHdhdGNobGlzdD13YXRjaGxpc3QsIGV2YWxfbWV0cmljID0gImVycm9yIiwgZXZhbF9tZXRyaWMgPSAibG9nbG9zcyIsIG9iamVjdGl2ZSA9ICJiaW5hcnk6bG9naXN0aWMiKQ0KYGBgDQoNCuWvueS6juWwj+eahOaVsOaNrumbhu+8jOeUseS6juecn+WunuaDheWGteWPr+iDveaYr+e6v+aAp+WPr+WIhueahO+8jOe6v+aAp+aPkOWNh+WZqOeahOaViOaenOWPr+iDveabtOWlveOAguaJgOS7peWcqOW7uuaooeaXtu+8jOW7uuiuruWwneivleS4pOenjeaPkOWNh+WZqOW5tuavlOi+g+aViOaenOOAgg0KDQojIDcg5pON57q1YHhnYi5ETWF0cml4YA0KKyDkvb/nlKhgeGdiLkRNYXRyaXguc2F2ZWDlrZjlgqhgeGdiLkRNYXRyaXhg5a+56LGhDQpgYGB7cn0NCnhnYi5ETWF0cml4LnNhdmUoZHRyYWluLCAiZHRyYWluLmJ1ZmZlciIpDQpgYGANCg0KKyDkvb/nlKhgeGdiLkRNYXRyaXhg6L295YWlYHhnYi5ETWF0cml4YOWvueixoQ0KYGBge3J9DQojIHRvIGxvYWQgaXQgaW4sIHNpbXBseSBjYWxsIHhnYi5ETWF0cml4DQpkdHJhaW4yIDwtIHhnYi5ETWF0cml4KCJkdHJhaW4uYnVmZmVyIikNCmJzdCA8LSB4Z2IudHJhaW4oZGF0YT1kdHJhaW4yLCBtYXhfZGVwdGg9MiwgZXRhPTEsIG50aHJlYWQgPSAyLCBucm91bmRzPTIsIHdhdGNobGlzdD13YXRjaGxpc3QsIG9iamVjdGl2ZSA9ICJiaW5hcnk6bG9naXN0aWMiKQ0KYGBgDQoNCisg5Yig6ZmkYHhnYi5ETWF0cml4YOWvueixoQ0KYGBge3J9DQpmaWxlLnJlbW92ZSgiZHRyYWluLmJ1ZmZlciIpDQpgYGANCg0KKyDkvb/nlKhgZ2V0aW5mb2Dmj5Dlj5ZgeGdiLkRNYXRyaXhg5a+56LGh5Lit55qE5L+h5oGv77yIJ2xhYmVsJywgJ3dlaWdodCcsICdiYXNlX21hcmdpbicsICducm93J++8iQ0KYGBge3J9DQpsYWJlbCA9IGdldGluZm8oZHRlc3QsICJsYWJlbCIpDQpwcmVkIDwtIHByZWRpY3QoYnN0LCBkdGVzdCkNCmVyciA8LSBhcy5udW1lcmljKHN1bShhcy5pbnRlZ2VyKHByZWQgPiAwLjUpICE9IGxhYmVsKSkvbGVuZ3RoKGxhYmVsKQ0KcHJpbnQocGFzdGUoInRlc3QtZXJyb3I9IiwgZXJyKSkNCmBgYA0KDQojIDgg6KeC5a+f54m55b6B6YeN6KaB5oCn5ZKM5qih5Z6LDQorIOeJueW+gemHjeimgeaAp+ivhOWIhg0KYGBge3J9DQppbXBvcnRhbmNlX21hdHJpeCA8LSB4Z2IuaW1wb3J0YW5jZShtb2RlbCA9IGJzdCkNCnByaW50KGltcG9ydGFuY2VfbWF0cml4KQ0KYGBgDQoNCmBgYHtyfQ0KeGdiLnBsb3QuaW1wb3J0YW5jZShpbXBvcnRhbmNlX21hdHJpeCA9IGltcG9ydGFuY2VfbWF0cml4KQ0KYGBgDQoNCisgYHhnYi5kdW1wYOWwhuWtpuW+l+eahOaooeWei+WvvOWFpXRleHTmlofku7YNCmBgYHtyfQ0KeGdiLmR1bXAoYnN0LCB3aXRoX3N0YXRzID0gVCkNCmBgYA0KDQorIGB4Z2IucGxvdC50cmVlYOe7mOWItuWtpuW+l+eahOaooeWeiw0KYGBge3J9DQpsaWJyYXJ5KERpYWdyYW1tZVIpDQpgYGANCg0KYGBge3J9DQp4Z2IucGxvdC50cmVlKG1vZGVsID0gYnN0KQ0KYGBgDQoNCisg5L+d5a2Y5ZKM6L295YWl5qih5Z6LDQpgYGB7cn0NCiMgc2F2ZSBtb2RlbCB0byBiaW5hcnkgbG9jYWwgZmlsZQ0KeGdiLnNhdmUoYnN0LCAieGdib29zdC5tb2RlbCIpDQpgYGANCg0KYGBge3J9DQojIGxvYWQgYmluYXJ5IG1vZGVsIHRvIFINCmJzdDIgPC0geGdiLmxvYWQoInhnYm9vc3QubW9kZWwiKQ0KYGBgDQoNCmBgYHtyfQ0KcHJlZDIgPC0gcHJlZGljdChic3QyLCB0ZXN0JGRhdGEpDQpgYGANCg0K5q+U6L6D5Y6f5aeL5qih5Z6L5ZKM6L295YWl55qE5qih5Z6L5piv5ZCm5a6M5YWo5LiA6Ie0DQpgYGB7cn0NCnByaW50KHBhc3RlKCJzdW0oYWJzKHByZWQyLXByZWQpKT0iLCBzdW0oYWJzKHByZWQyLXByZWQpKSkpDQpgYGANCg0KYGBge3J9DQpmaWxlLnJlbW92ZSgiLi94Z2Jvb3N0Lm1vZGVsIikNCmBgYA0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg==